﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using LogicalComponent.util;
using LogicalComponent.util.exception;

namespace LogicalComponent.rules.grid
{
    /// <summary>
    /// Enumeration of different types of movement modifiers.
    /// </summary>
    public enum MoveCostModType
    {
        IMPASSABLE,
        MULTIPLIER,
        ADDER
    }

    public abstract class MoveRuleSet
    {

        /// <summary>
        /// Gets the grid for the move rule set.
        /// </summary>
        public abstract IGrid Grid { get; }

        /// <summary>
        /// Gets or sets a value indicating whether [needs full cost].
        /// </summary>
        /// <value>
        ///   <c>true</c> if [needs full cost]; otherwise, <c>false</c>.
        /// </value>
        public abstract Boolean NeedsFullCost { get; set; }

        /// <summary>
        /// Gets the hueristic for going from one tile to another accross adjacent tiles.
        /// </summary>
        /// <param name="source">The source.</param>
        /// <param name="destination">The destination.</param>
        /// <returns>The minimum cost of moving to that tile</returns>
        public abstract Fraction GetHueristic(Coordinate source, Coordinate target);

        /// <summary>
        /// Gets the cost of going from one tile to an adjeacent tile.  Will raise
        /// an exception if the tiles are not adjacent.
        /// </summary>
        /// <param name="source">The source.</param>
        /// <param name="destination">The destination.</param>
        /// <returns>The cost of moving to the adjacent tile</returns>
        public abstract Fraction GetCost(Coordinate source, Coordinate destination);

        /// <summary>
        /// Gets the neighbors of a given coordinate.
        /// </summary>
        /// <param name="source">The coordinate.</param>
        /// <returns>
        /// The list of coordinates that are neighboring the coordinate 
        /// given.
        /// </returns>
        public abstract Coordinate[] GetNeighbors(Coordinate coordinate);

        /// <summary>
        /// Gets the cost to move onto a tile.
        /// </summary>
        /// <param name="tile">The tile.</param>
        /// <param name="baseCost">The base cost.</param>
        /// <returns></returns>
        protected Fraction CalculateCost(ITile tile, Fraction baseCost)
        {
            if (baseCost < 0)
            {
                return baseCost;
            }
            switch (tile.MoveModType) 
            {
                case MoveCostModType.IMPASSABLE:
                    return new Fraction(-1);
                case MoveCostModType.ADDER:
                    return baseCost + tile.MoveModValue;
                case MoveCostModType.MULTIPLIER:
                    return baseCost * tile.MoveModValue;
                default:
                    throw new LogicalUserError("Tile was not set with a valid move cost type");
            }
        }    
    }

    public class SquareMoveRuleSet : MoveRuleSet
    {
        // *********************************************************************
        // **************** PROPERTIES *****************************************
        // *********************************************************************

        private Fraction minOrthagonalCost;
        private Fraction minDiagonalCost;
        private Fraction diagonalDifference;
        private IGrid grid;

        /// <summary>
        /// Gets or sets the min orthagonal cost.
        /// </summary>
        /// <value>
        /// The min orthagonal cost.
        /// </value>
        public Fraction MinOrthagonalCost 
        { 
            get { return minOrthagonalCost; }
            set  
            {
                this.validateMinCosts(value, this.minDiagonalCost);
                this.minOrthagonalCost = value;
                this.diagonalDifference = this.minDiagonalCost - this.MinOrthagonalCost;
            }
        }

        /// <summary>
        /// Gets or sets the min diagonal cost.
        /// </summary>
        /// <value>
        /// The min diagonal cost.
        /// </value>
        public Fraction MinDiagonalCost
        { 
            get { return minDiagonalCost; }
            set  
            {
                this.validateMinCosts(this.minOrthagonalCost, value);
                this.minOrthagonalCost = value;
                this.diagonalDifference = this.minDiagonalCost - this.MinOrthagonalCost;
            }
        }

        /// <inheritdoc/>
        public override IGrid Grid { get { return this.grid; } }

        /// <summary>
        /// Gets or sets the orthagonal cost.
        /// </summary>
        /// <value>
        /// The orthagonal cost.
        /// </value>
        public Fraction OrthagonalCost { get; set; }

        /// <summary>
        /// Gets or sets the diagonal cost.
        /// </summary>
        /// <value>
        /// The diagonal cost.
        /// </value>
        public Fraction DiagonalCost { get; set; }

        /// <summary>
        /// Gets or sets the anywhere cost.
        /// </summary>
        /// <value>
        /// The anywhere cost.
        /// </value>
        public Fraction AnywhereCost { get; set; }

        /// <inheritdoc/>
        public override Boolean NeedsFullCost { get; set; }

        // *********************************************************************
        // **************** CONSTRUCTORS ***************************************
        // *********************************************************************

        /// <summary>
        /// Initializes a new instance of the <see cref="SquareMoveRuleSet"/> class.
        /// </summary>
        public SquareMoveRuleSet(IGrid grid)
        {
            this.grid = grid;
            this.minOrthagonalCost = new Fraction(1);
            this.minDiagonalCost = new Fraction(1);
            this.diagonalDifference = this.minDiagonalCost - this.minOrthagonalCost;
            this.OrthagonalCost = new Fraction(-1);
            this.DiagonalCost = new Fraction(-1);
            this.AnywhereCost = new Fraction(-1);
            this.NeedsFullCost = true;
        }

        // *********************************************************************
        // **************** PUBLIC *********************************************
        // *********************************************************************

        /// <inheritdoc/>
        public override Fraction GetCost(Coordinate source, Coordinate destination)
        {
            if (!(this.Grid.IsOnGrid(source) && this.Grid.IsOnGrid(destination)))
            {
                throw new LogicalUserError("Attempted to get cost for tiles that are not on the grid");
            }
            Coordinate diff = destination - source;
            int orthagonalSum = diff.X * diff.X + diff.Z * diff.Z;
            ITile tile = this.Grid.GetTile(destination);
            if (orthagonalSum == 1)
            {
                return CalculateCost(tile, this.OrthagonalCost);
            }
            else if (orthagonalSum == 2)
            {
                return CalculateCost(tile, this.DiagonalCost);
            }
            throw new LogicalUserError(String.Format(
                "Attempted to get cost between 2 non-adjacent tiles: {0} and {1}, {2}",
                source, destination, this.GetNeighbors(source)));
        }

        /// <inheritdoc/>
        public override Coordinate[] GetNeighbors(Coordinate coordinate)
        {
            
            return new Coordinate[] {
                coordinate.North, coordinate.NorthEast, coordinate.NorthWest, 
                coordinate.South, coordinate.SouthEast, coordinate.SouthWest,
                coordinate.West, coordinate.East
            };
        }

        /// <inheritdoc/>
        public override Fraction GetHueristic(Coordinate source, Coordinate destination)
        {
            Coordinate diff = destination - source;
            int xDistance = Math.Abs(diff.X);
            int zDistance = Math.Abs(diff.Z);
            bool xltz = xDistance < zDistance;
            int major = Math.Abs(xltz ? zDistance : xDistance);
            int minor = Math.Abs(xltz ? xDistance : zDistance);
            return major * this.minOrthagonalCost + minor * diagonalDifference;
        }

        // *********************************************************************
        // **************** PRIVATE ********************************************
        // *********************************************************************

        private void validateMinCosts(Fraction orthagonal, Fraction diagonal)
        {
            if (diagonal > orthagonal * 2 || diagonal < orthagonal)
            {
                throw new LogicalUserError(string.Format(
                   "Path finding requires orthagonalCost <= diagonalCost <= 2 * " +
                   "orthagonalCost in order to worrk correctly. Orthagonal = " +
                   "{0}, diagonal = {1} ", orthagonal, orthagonal));
            }
        }

        protected Fraction GetCostTo(ITile tile, Fraction baseCost)
        {
            switch (tile.MoveModType) 
            {
                case MoveCostModType.IMPASSABLE:
                    return new Fraction(-1);
                case MoveCostModType.ADDER:
                    return baseCost + tile.MoveModValue;
                case MoveCostModType.MULTIPLIER:
                    return baseCost * tile.MoveModValue;
                default:
                    throw new LogicalUserError("Tile was not set with a valid move cost type");
            }
        }
    }

    public class HexMoveRuleSet : MoveRuleSet
    {
        // *********************************************************************
        // **************** PROPERTIES *****************************************
        // *********************************************************************

        private IGrid grid;

        /// <inheritdoc/>
        public override IGrid Grid { get { return this.grid; } }

        /// <summary>
        /// Gets or sets the base cost.
        /// </summary>
        /// <value>
        /// The diagonal cost.
        /// </value>
        public Fraction BaseCost { get; set; }

        /// <inheritdoc/>
        public override Boolean NeedsFullCost { get; set; }

        // *********************************************************************
        // **************** CONSTRUCTORS ***************************************
        // *********************************************************************

        /// <summary>
        /// Initializes a new instance of the <see cref="SquareMoveRuleSet"/> class.
        /// </summary>
        public HexMoveRuleSet(IGrid grid)
        {
            this.grid = grid;
            this.NeedsFullCost = true;
            this.BaseCost = new Fraction(-1);
        }


        // *********************************************************************
        // **************** PUBLIC *********************************************
        // *********************************************************************

        /// <inheritdoc/>
        public override Fraction GetCost(Coordinate source, Coordinate destination)
        {
            if (!(this.Grid.IsOnGrid(source) && this.Grid.IsOnGrid(destination)))
            {
                throw new LogicalUserError("Attempted to get cost for tiles that are not on the grid");
            }
            Coordinate diff = destination - source;
            int diffSum = diff.X + diff.Z;
            ITile tile = this.Grid.GetTile(destination);
            if (diffSum == 1 || diffSum == -1)
            {
                return CalculateCost(tile, this.BaseCost);
            }
            else if (diffSum == 0 && Math.Abs(diff.X) == 1)
            {
                return CalculateCost(tile, this.BaseCost);
            }
            throw new LogicalUserError(String.Format(
                "Attempted to get cost between 2 non-adjacent tiles: {0} and {1}, {2}",
                source, destination, this.GetNeighbors(source)));
        }

        /// <inheritdoc/>
        public override Coordinate[] GetNeighbors(Coordinate coordinate)
        {
            return new Coordinate[] {
                coordinate.HexNorth, coordinate.HexNorthEast, coordinate.HexNorthWest, 
                coordinate.HexSouth, coordinate.HexSouthEast, coordinate.HexSouthWest
            };
        }

        /// <inheritdoc/>
        public override Fraction GetHueristic(Coordinate source, Coordinate destination)
        {
            Coordinate diff = destination - source;
            int xDistance = Math.Abs(diff.X);
            if (diff.Z == 0)
            {
                return this.BaseCost * xDistance;

            }
            int zDistance = Math.Abs(diff.Z);
            if (diff.X / diff.Z < 0)
            {
                bool xltz = xDistance < zDistance;
                return this.BaseCost * (xltz ? zDistance : xDistance); 
            }
            return this.BaseCost * (xDistance + zDistance);
        }

        // *********************************************************************
        // **************** PRIVATE ********************************************
        // *********************************************************************
        
        /// <inheritdoc/>
        protected Fraction GetCostTo(ITile tile, Fraction baseCost)
        {
            switch (tile.MoveModType) 
            {
                case MoveCostModType.IMPASSABLE:
                    return new Fraction(-1);
                case MoveCostModType.ADDER:
                    return baseCost + tile.MoveModValue;
                case MoveCostModType.MULTIPLIER:
                    return baseCost * tile.MoveModValue;
                default:
                    throw new LogicalUserError("Tile was not set with a valid move cost type");
            }
        }
    }
}